跳到主要内容

Java 多线程-常见问题

synchronized 的实现原理

★ synchronized作用于「方法」或者「代码块」,保证被修饰的代码在同一时间只能被一个线程访问。

synchronized修饰代码块时,JVM采用「monitorenter、monitorexit」两个指令来实现同步 synchronized修饰同步方法时,JVM采用「ACC_SYNCHRONIZED」标记符来实现同步 monitorenter、monitorexit或者ACC_SYNCHRONIZED都是「基于Monitor实现」的 实例对象里有对象头,对象头里面有Mark Word,Mark Word指针指向了「monitor」 Monitor其实是一种「同步工具」,也可以说是一种「同步机制」。

synchronized 的锁优化

  • 偏向锁:在无竞争的情况下,把整个同步都消除掉,CAS操作都不做,且将来只有第一个申请锁的线程会使用锁。
  • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。在没有多线程竞争时,使用 CAS 操作来保证安全,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争(CAS 失败),这时除了互斥量本身开销,还额外有 CAS 操作的开销,多次失败后升级为重量锁(这个次数看操作系统)
  • 重量级锁:有实际竞争,且锁竞争时间长。
    • 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
    • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间(上下文切换需要时间),所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。

ThreadLocal 原理

ThreadLocal,即线程本地变量。如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

  • Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 本身,value 是 ThreadLocal 的泛型值。
  • 每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。

ThreadLocal 内存泄露问题

弱引用比较容易被回收。因此,如果 ThreadLocal(ThreadLocalMap 的 Key)被垃圾回收器回收了,但是因为 ThreadLocalMap 生命周期和 Thread 是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap 的 key 没了,value 还在,这就会「造成了内存泄漏问题」。

如何「解决内存泄漏问题」?使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。

ThreadLocal 的应用场景

  • 数据库连接池
  • 会话管理中使用

synchronized 和 ReentrantLock 的区别?

锁的实现:synchronized 是 Java 语言的关键字,基于 JVM 实现。而 ReentrantLock 是基于 JDK 的 API 层面实现的(一般是 lock()unlock() 方法配合 try/finally 语句块来完成。)

性能:在 JDK1.6 锁优化以前,synchronized 的性能比 ReenTrantLock 差很多。但是 JDK6 开始,增加了适应性自旋、锁消除等,两者性能就差不多了。

功能特点:ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。

  • ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。
  • ReentrantLock 可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  • synchronized 与 wait() 和 notify()/notifyAll() 方法结合实现等待/通知机制,ReentrantLock 类借助 Condition 接口与 newCondition() 方法实现。
  • ReentrantLock 需要手工声明来加锁和释放锁,一般跟 finally 配合释放锁。而 synchronized 不用手动释放锁。

例如下面的操作使用 synchronized 对 count 操作进行加锁

public class Counter {
private int count;

public synchronized void add(int n) {
count += n;
}
}

使用 ReentrantLock 进行替换

public class Counter {
private final Lock lock = new ReentrantLock();
private int count;

public void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
}

CountDownLatch与CyclicBarrier区别

CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行; CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

CAS 有什么缺陷,如何解决?

CAS,Compare and Swap,比较并交换;

CAS 涉及3个操作数,内存地址值V,预期原值A,新值B;如果内存位置的值V与预期原A值相匹配,就更新为新值B,否则不更新

CAS有什么缺陷?

ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

可以通过 AtomicStampedReference(原子记号引用)解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证 CAS 的正确性。

循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。

很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

只能保证一个变量的原子操作。

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

可以通过这两个方式解决这个问题:

  • 使用互斥锁来保证原子性;
  • 将多个变量封装成对象,通过 AtomicReference 来保证原子性。

如何保证多线程下i++ 结果正确?

使用循环 CAS,实现 i++原子操作 使用锁机制,实现 i++原子操作 使用 synchronized,实现 i++原子操作